/*
 * Copyright 2012 bigbiff/Dees_Troy TeamWin
 * This file is part of TWRP/TeamWin Recovery Project.
 *
 * TWRP is free software: you can redistribute it and/or modify
 * it under the terms of the GNU General Public License as published by
 * the Free Software Foundation, either version 3 of the License, or
 * (at your option) any later version.
 *
 * TWRP is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 * GNU General Public License for more details.
 *
 * You should have received a copy of the GNU General Public License
 * along with TWRP.  If not, see <http://www.gnu.org/licenses/>.
 */

#include "gui/keyboard.hpp"

#include <cstring>

#include <linux/input.h>

#include "mblog/logging.h"

#include "data.hpp"
#include "variables.h"

#include "gui/gui.h"

#define LOG_TAG "mbbootui/gui/keyboard"

bool GUIKeyboard::CtrlActive = false;

GUIKeyboard::GUIKeyboard(xml_node<>* node)
    : GUIObject(node)
{
    int layoutindex, rowindex, keyindex, Xindex, Yindex, keyHeight = 0, keyWidth = 0;
    currentKey = nullptr;
    highlightRenderCount = 0;
    hasHighlight = hasCapsHighlight = hasCtrlHighlight = false;
    char resource[10], layout[8], row[5], key[6], longpress[7];
    xml_attribute<>* attr;
    xml_node<>* child;
    xml_node<>* keylayout;
    xml_node<>* keyrow;

    for (layoutindex=0; layoutindex<MAX_KEYBOARD_LAYOUTS; layoutindex++) {
        layouts[layoutindex].keyboardImg = nullptr;
        memset(layouts[layoutindex].keys, 0, sizeof(Layout::keys));
        memset(layouts[layoutindex].row_end_y, 0, sizeof(Layout::row_end_y));
    }

    mRendered = false;
    currentLayout = 1;
    CapsLockOn = false;

    if (!node) {
        return;
    }

    mHighlightColor = LoadAttrColor(FindNode(node, "highlight"), "color", &hasHighlight);
    mCapsHighlightColor = LoadAttrColor(FindNode(node, "capshighlight"), "color", &hasCapsHighlight);
    mCtrlHighlightColor = LoadAttrColor(FindNode(node, "ctrlhighlight"), "color", &hasCtrlHighlight);

    child = FindNode(node, "keymargin");
    mKeyMarginX = LoadAttrIntScaleX(child, "x", 0);
    mKeyMarginY = LoadAttrIntScaleY(child, "y", 0);

    child = FindNode(node, "background");
    mBackgroundColor = LoadAttrColor(child, "color", COLOR(32,32,32,255));

    child = FindNode(node, "key-alphanumeric");
    mFont = PageManager::GetResources()->FindFont(LoadAttrString(child, "font", "keylabel"));
    mFontColor = LoadAttrColor(child, "textcolor", COLOR(255,255,255,255));
    mKeyColorAlphanumeric = LoadAttrColor(child, "color", COLOR(0,0,0,0));

    child = FindNode(node, "key-other");
    mSmallFont = PageManager::GetResources()->FindFont(LoadAttrString(child, "font", "keylabel-small"));
    mFontColorSmall = LoadAttrColor(child, "textcolor", COLOR(192,192,192,255));
    mKeyColorOther = LoadAttrColor(child, "color", COLOR(0,0,0,0));

    child = FindNode(node, "longpress");
    mLongpressFont = PageManager::GetResources()->FindFont(LoadAttrString(child, "font", "keylabel-longpress"));
    mLongpressFontColor = LoadAttrColor(child, "textcolor", COLOR(128,128,128,255));
    LoadPlacement(child, &longpressOffsetX, &longpressOffsetY);

    LoadKeyLabels(node, 0); // load global key labels

    // compatibility ugliness: resources should be specified in the layouts themselves instead
    // Load the images for the different layouts
    child = FindNode(node, "layout");
    if (child) {
        layoutindex = 1;
        strcpy(resource, "resource1");
        attr = child->first_attribute(resource);
        while (attr && layoutindex < (MAX_KEYBOARD_LAYOUTS + 1)) {
            layouts[layoutindex - 1].keyboardImg = LoadAttrImage(child, resource);

            layoutindex++;
            resource[8] = (char) (layoutindex + 48);
            attr = child->first_attribute(resource);
        }
    }

    // Check the first image to get height and width
    if (layouts[0].keyboardImg && layouts[0].keyboardImg->GetResource()) {
        mRenderW = layouts[0].keyboardImg->GetWidth();
        mRenderH = layouts[0].keyboardImg->GetHeight();
    }

    // Load all of the layout maps
    layoutindex = 1;
    strcpy(layout, "layout1");
    keylayout = FindNode(node, layout);
    while (keylayout) {
        if (layoutindex > MAX_KEYBOARD_LAYOUTS) {
            LOGE("Too many layouts defined in keyboard.");
            return;
        }

        LoadKeyLabels(keylayout, layoutindex); // load per-layout key labels

        Layout& lay = layouts[layoutindex - 1];

        child = keylayout->first_node("keysize");
        keyHeight = LoadAttrIntScaleY(child, "height", 0);
        keyWidth = LoadAttrIntScaleX(child, "width", 0);
        // compatibility ugliness: capslock="0" means that this is the caps layout. Also it has nothing to do with keysize.
        lay.is_caps = (LoadAttrInt(child, "capslock", 1) == 0);
        // compatibility ugliness: revert_layout has nothing to do with keysize.
        lay.revert_layout = LoadAttrInt(child, "revert_layout", -1);

        rowindex = 1;
        Yindex = 0;
        strcpy(row, "row1");
        keyrow = keylayout->first_node(row);
        while (keyrow) {
            if (rowindex > MAX_KEYBOARD_ROWS) {
                LOGE("Too many rows defined in keyboard.");
                return;
            }

            Yindex += keyHeight;
            lay.row_end_y[rowindex - 1] = Yindex;

            keyindex = 1;
            Xindex = 0;
            strcpy(key, "key01");
            attr = keyrow->first_attribute(key);

            while (attr) {
                if (keyindex > MAX_KEYBOARD_KEYS) {
                    LOGE("Too many keys defined in a keyboard row.");
                    return;
                }

                const char* keyinfo = attr->value();

                if (strlen(keyinfo) == 0) {
                    LOGE("No key info on layout%i, row%i, key%dd.", layoutindex, rowindex, keyindex);
                    return;
                }

                if (ParseKey(keyinfo, lay.keys[rowindex - 1][keyindex - 1], Xindex, keyWidth, false)) {
                    LOGE("Invalid key info on layout%i, row%i, key%02i.", layoutindex, rowindex, keyindex);
                }


                // PROCESS LONG PRESS INFO IF EXISTS
                sprintf(longpress, "long%02i", keyindex);
                attr = keyrow->first_attribute(longpress);
                if (attr) {
                    const char* keyinfo = attr->value();

                    if (strlen(keyinfo) == 0) {
                        LOGE("No long press info on layout%i, row%i, long%dd.", layoutindex, rowindex, keyindex);
                        return;
                    }

                    if (ParseKey(keyinfo, lay.keys[rowindex - 1][keyindex - 1], Xindex, keyWidth, true)) {
                        LOGE("Invalid long press key info on layout%i, row%i, long%02i.", layoutindex, rowindex, keyindex);
                    }
                }
                keyindex++;
                sprintf(key, "key%02i", keyindex);
                attr = keyrow->first_attribute(key);
            }
            rowindex++;
            row[3] = (char)(rowindex + 48);
            keyrow = keylayout->first_node(row);
        }
        layoutindex++;
        layout[6] = (char)(layoutindex + 48);
        keylayout = FindNode(node, layout);
    }

    int x, y;
    // Load the placement
    LoadPlacement(FindNode(node, "placement"), &x, &y, &mRenderW, &mRenderH);
    SetRenderPos(x, y, mRenderW, mRenderH);
    return;
}

GUIKeyboard::~GUIKeyboard()
{
}

int GUIKeyboard::ParseKey(const char* keyinfo, Key& key, int& Xindex, int keyWidth, bool longpress)
{
    key.layout = 0;
    int keychar = 0;
    if (strlen(keyinfo) == 1) {
        // This is a single key, simple definition
        keychar = keyinfo[0];
    } else {
        // This key has extra data: {keywidth}:{what_the_key_does}
        keyWidth = scale_theme_x(atoi(keyinfo));

        const char* ptr = keyinfo;
        while (*ptr > 32 && *ptr != ':') {
            ptr++;
        }
        if (*ptr != ':') {
            return -1;  // no colon is an error
        }
            ptr++;

        if (*ptr == 0) {  // This is an empty area
            keychar = 0;
        } else if (strlen(ptr) == 1) {  // This is the character that this key uses
            keychar = *ptr;
        } else if (*ptr == 'c') {  // This is an ASCII character code: "c:{number}"
            keychar = atoi(ptr + 2);
        } else if (*ptr == 'k') {  // This is a Linux keycode from input.h: "k:{number}"
            keychar = -atoi(ptr + 2);
        } else if (*ptr == 'l') {  // This is a different layout: "layout{number}"
            key.layout = atoi(ptr + 6);
        } else if (*ptr == 'a') {  // This is an action: "action" (the Enter key)
            keychar = KEYBOARD_ACTION;
        } else {
            return -1;
        }
    }

    if (longpress) {
        key.longpresskey = keychar;
    } else {
        key.key = keychar;
        Xindex += keyWidth;
        key.end_x = Xindex - 1;
    }

    return 0;
}

void GUIKeyboard::LoadKeyLabels(xml_node<>* parent, int layout)
{
    for (xml_node<>* child = parent->first_node(); child; child = child->next_sibling()) {
        std::string name = child->name();
        if (name == "keylabel") {
            std::string keydef = LoadAttrString(child, "key", "");
            Key tempkey;
            int dummyX;
            if (ParseKey(keydef.c_str(), tempkey, dummyX, 0, false) == 0) {
                KeyLabel keylabel;
                keylabel.key = tempkey.key;
                keylabel.layout_from = layout;
                keylabel.layout_to = tempkey.layout;
                keylabel.text = LoadAttrString(child, "text", "");
                keylabel.image = LoadAttrImage(child, "resource");
                mKeyLabels.push_back(keylabel);
            } else {
                LOGE("Ignoring invalid keylabel in layout %d: '%s'.", layout, keydef.c_str());
            }
        }
    }
}

void GUIKeyboard::DrawKey(Key& key, int keyX, int keyY, int keyW, int keyH)
{
    int keychar = key.key;
    if (!keychar && !key.layout) {
        return;
    }

    // key background
    COLOR& c = (keychar >= 32 && keychar < 127) ? mKeyColorAlphanumeric : mKeyColorOther;
    gr_color(c.red, c.green, c.blue, c.alpha);
    keyX += mKeyMarginX;
    keyY += mKeyMarginY;
    keyW -= mKeyMarginX * 2;
    keyH -= mKeyMarginY * 2;
    gr_fill(keyX, keyY, keyW, keyH);

    // key label
    FontResource* labelFont = mFont;
    std::string labelText;
    ImageResource* labelImage = nullptr;
    if (keychar > 32 && keychar < 127) {
        // TODO: this will eventually need UTF-8 support
        labelText = (char) keychar;
        if (CtrlActive) {
            int ctrlchar = KeyCharToCtrlChar(keychar);
            if (ctrlchar != keychar) {
                labelText = std::string("^") + (char)(ctrlchar + 64);
            }
        }
        gr_color(mFontColor.red, mFontColor.green, mFontColor.blue, mFontColor.alpha);
    } else {
        // search for a special key label
        for (auto it = mKeyLabels.begin(); it != mKeyLabels.end(); ++it) {
            if (it->layout_from > 0 && it->layout_from != currentLayout) {
                continue; // this label is for another layout
            }
            if (it->key == key.key && it->layout_to == key.layout) {
                // found a label
                labelText = it->text;
                labelImage = it->image;
                break;
            }
        }
        labelFont = mSmallFont;
        gr_color(mFontColorSmall.red, mFontColorSmall.green, mFontColorSmall.blue, mFontColorSmall.alpha);
    }

    if (labelImage) {
        int w = labelImage->GetWidth();
        int h = labelImage->GetHeight();
        int x = keyX + (keyW - w) / 2;
        int y = keyY + (keyH - h) / 2;
        gr_blit(labelImage->GetResource(), 0, 0, w, h, x, y);
    } else if (!labelText.empty()) {
        void* fontResource = labelFont->GetResource();
        int textW = gr_ttf_measureEx(labelText.c_str(), fontResource);
        int textH = labelFont->GetHeight();
        int textX = keyX + (keyW - textW) / 2;
        int textY = keyY + (keyH - textH) / 2;
        gr_textEx_scaleW(textX, textY, labelText.c_str(), fontResource, keyW, TOP_LEFT, 0);
    }

    // longpress key label (only if font is defined)
    keychar = key.longpresskey;
    if (keychar > 32 && keychar < 127 && mLongpressFont->GetResource()) {
        void* fontResource = mLongpressFont->GetResource();
        gr_color(mLongpressFontColor.red, mLongpressFontColor.green, mLongpressFontColor.blue, mLongpressFontColor.alpha);
        std::string text(1, keychar);
        __attribute__((unused))
        int textH = mLongpressFont->GetHeight();
        int textW = gr_ttf_measureEx(text.c_str(), fontResource);
        int textX = keyX + keyW - longpressOffsetX - textW;
        int textY = keyY + longpressOffsetY;
        gr_textEx_scaleW(textX, textY, text.c_str(), fontResource, keyW, TOP_LEFT, 0);
    }
}

int GUIKeyboard::KeyCharToCtrlChar(int key)
{
    // convert upper and lower case to ctrl chars
    // Ctrl+A to Ctrl+_ (we don't support entering null bytes)
    if (key >= 65 && key <= 127 && key != 96) {
        return key & 0x1f;
    }
    return key; // pass on others (already ctrl chars, numbers, etc.) unchanged
}

int GUIKeyboard::Render()
{
    if (!isConditionTrue()) {
        mRendered = false;
        return 0;
    }

    Layout& lay = layouts[currentLayout - 1];

    bool drawKeys = false;
    if (lay.keyboardImg && lay.keyboardImg->GetResource()) {
        // keyboard is image based
        gr_blit(lay.keyboardImg->GetResource(), 0, 0, mRenderW, mRenderH, mRenderX, mRenderY);
    } else {
        // keyboard is software drawn
        // fill background
        gr_color(mBackgroundColor.red, mBackgroundColor.green, mBackgroundColor.blue, mBackgroundColor.alpha);
        gr_fill(mRenderX, mRenderY, mRenderW, mRenderH);
        drawKeys = true;
    }

    // draw keys
    int y1 = 0;
    for (int row = 0; row < MAX_KEYBOARD_ROWS; ++row) {
        int rowY = mRenderY + y1;
        int rowH = lay.row_end_y[row] - y1;
        y1 = lay.row_end_y[row];
        int x1 = 0;
        for (int col = 0; col < MAX_KEYBOARD_KEYS; ++col) {
            Key& key = lay.keys[row][col];
            int keyY = rowY;
            int keyH = rowH;
            int keyX = mRenderX + x1;
            int keyW = key.end_x - x1;
            x1 = key.end_x;

            // Draw key for software drawn keyboard
            if (drawKeys) {
                DrawKey(key, keyX, keyY, keyW, keyH);
            }

            // Draw highlight for capslock
            if (hasCapsHighlight && lay.is_caps && CapsLockOn && key.layout > 0 && key.layout == lay.revert_layout) {
                gr_color(mCapsHighlightColor.red, mCapsHighlightColor.green, mCapsHighlightColor.blue, mCapsHighlightColor.alpha);
                gr_fill(keyX, keyY, keyW, keyH);
            }

            // Draw highlight for control
            if (hasCtrlHighlight && key.key == -KEY_LEFTCTRL && CtrlActive) {
                gr_color(mCtrlHighlightColor.red, mCtrlHighlightColor.green, mCtrlHighlightColor.blue, mCtrlHighlightColor.alpha);
                gr_fill(keyX, keyY, keyW, keyH);
            }

            // Highlight current key
            if (hasHighlight && &key == currentKey && highlightRenderCount != 0) {
                gr_color(mHighlightColor.red, mHighlightColor.green, mHighlightColor.blue, mHighlightColor.alpha);
                gr_fill(keyX, keyY, keyW, keyH);
            }
        }
    }

    if (!hasHighlight || highlightRenderCount == 0) {
        mRendered = true;
    } else if (highlightRenderCount > 0) {
        highlightRenderCount--;
    }
    return 0;
}

int GUIKeyboard::Update()
{
    if (!isConditionTrue()) {
        return (mRendered ? 2 : 0);
    }
    if (!mRendered) {
        return 2;
    }

    return 0;
}

int GUIKeyboard::SetRenderPos(int x, int y, int w, int h)
{
    RenderObject::SetRenderPos(x, y, w, h);
    SetActionPos(mRenderX, mRenderY, mRenderW, mRenderH);
    return 0;
}

GUIKeyboard::Key* GUIKeyboard::HitTestKey(int x, int y)
{
    if (!IsInRegion(x, y)) {
        return nullptr;
    }

    int rely = y - mRenderY;
    int relx = x - mRenderX;

    Layout& lay = layouts[currentLayout - 1];

    // Find the correct row
    int row;
    for (row = 0; row < MAX_KEYBOARD_ROWS; ++row) {
        if (lay.row_end_y[row] > rely) {
            break;
        }
    }
    if (row == MAX_KEYBOARD_ROWS) {
        return nullptr;
    }

    // Find the correct key (column)
    int col;
    int x1 = 0;
    for (col = 0; col < MAX_KEYBOARD_KEYS; ++col) {
        Key& key = lay.keys[row][col];
        if (x1 <= relx && relx < key.end_x && (key.key != 0 || key.layout != 0)) {
            // This is the key that was pressed!
            return &key;
        }
        x1 = key.end_x;
    }
    return nullptr;
}

int GUIKeyboard::NotifyTouch(TOUCH_STATE state, int x, int y)
{
    static int was_held = 0, startX = 0;

    if (!isConditionTrue()) {
        return -1;
    }

    switch (state) {
    case TOUCH_START:
        was_held = 0;
        startX = x;
        currentKey = HitTestKey(x, y);
        if (currentKey) {
            highlightRenderCount = -1;
        } else {
            highlightRenderCount = 0;
        }
        mRendered = false;
        break;

    case TOUCH_DRAG:
        break;

    case TOUCH_RELEASE:
        // TODO: we might want to notify of key releases here
        if (x < startX - (mRenderW * 0.5)) {
            if (highlightRenderCount != 0) {
                highlightRenderCount = 0;
                mRendered = false;
            }
            PageManager::NotifyCharInput(KEYBOARD_SWIPE_LEFT);
            return 0;
        } else if (x > startX + (mRenderW * 0.5)) {
            if (highlightRenderCount != 0) {
                highlightRenderCount = 0;
                mRendered = false;
            }
            PageManager::NotifyCharInput(KEYBOARD_SWIPE_RIGHT);
            return 0;
        }
        // fall through
    case TOUCH_HOLD:
    case TOUCH_REPEAT:
        if (!currentKey) {
            if (highlightRenderCount != 0) {
                highlightRenderCount = 0;
                mRendered = false;
            }
            return 0;
        }

        if (highlightRenderCount != 0) {
            if (state == TOUCH_RELEASE) {
                highlightRenderCount = 2;
            } else {
                highlightRenderCount = -1;
            }
            mRendered = false;
        }

        if (HitTestKey(x, y) != currentKey) {
            // We dragged off of the starting key
            currentKey = nullptr;
            if (highlightRenderCount != 0) {
                highlightRenderCount = 0;
                mRendered = false;
            }
            return 0;
        } else {
            Key& key = *currentKey;
            bool repeatKey = false;
            Layout& lay = layouts[currentLayout - 1];
            if (state == TOUCH_RELEASE && was_held == 0) {
                DataManager::Vibrate(VAR_TW_KEYBOARD_VIBRATE);
                if (key.layout > 0) {
                    // Switch layouts
                    if (lay.is_caps && key.layout == lay.revert_layout && !CapsLockOn) {
                        CapsLockOn = true; // Set the caps lock
                    } else {
                        CapsLockOn = false; // Unset the caps lock and change layouts
                        currentLayout = key.layout;
                    }
                    mRendered = false;
                } else if (key.key == KEYBOARD_ACTION) {
                    // Action
                    highlightRenderCount = 0;
                    // Send action notification
                    PageManager::NotifyCharInput(key.key);
                } else if (key.key == -KEY_LEFTCTRL) {
                    CtrlActive = !CtrlActive; // toggle Control key state
                    mRendered = false; // render Ctrl key highlight
                } else if (key.key != 0) {
                    // Regular key
                    if (key.key > 0) {
                        // ASCII code or character
                        int keycode = key.key;
                        if (CtrlActive) {
                            CtrlActive = false;
                            mRendered = false;
                            keycode = KeyCharToCtrlChar(key.key);
                        }
                        PageManager::NotifyCharInput(keycode);
                    } else {
                        // Linux key code
                        PageManager::NotifyKey(-key.key, true);
                        PageManager::NotifyKey(-key.key, false);
                    }
                    if (!CapsLockOn && lay.is_caps) {
                        // caps lock was not set, change layouts
                        currentLayout = lay.revert_layout;
                        mRendered = false;
                    }
                }
            } else if (state == TOUCH_HOLD) {
                was_held = 1;
                if (key.longpresskey > 0) {
                    // Long Press Key
                    DataManager::Vibrate(VAR_TW_KEYBOARD_VIBRATE);
                    PageManager::NotifyCharInput(key.longpresskey);
                } else {
                    repeatKey = true;
                }
            } else if (state == TOUCH_REPEAT) {
                was_held = 1;
                repeatKey = true;
            }
            if (repeatKey) {
                if (key.key == KEYBOARD_BACKSPACE) {
                    // Repeat backspace
                    PageManager::NotifyCharInput(key.key);
                }
                switch (key.key) {
                // Repeat arrows
                case -KEY_LEFT:
                case -KEY_RIGHT:
                case -KEY_UP:
                case -KEY_DOWN:
                    PageManager::NotifyKey(-key.key, true);
                    PageManager::NotifyKey(-key.key, false);
                    break;
                }
            }
        }
        break;
    }

    return 0;
}

void GUIKeyboard::SetPageFocus(int inFocus)
{
    if (inFocus) {
        CtrlActive = false;
    }
}
